//
// JigLibXActor.cs
//
// This file is part of the QuickStart Engine's Wrapper to JigLibX. See http://www.codeplex.com/QuickStartEngine
// for license details.
//

using System;
using System.Collections.Generic;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

using JigLibX.Physics;
using JigLibX.Collision;
using JigLibX.Math;
using JigLibX.Geometry;
using JigLibX.Utils;

namespace Alex_s_Dream_The_Toy_World
{
    /// <summary>
    /// An implementation of the <see cref="IPhysicsActor"/> interface using JigLibX.
    /// </summary>
    public class JigLibXActor : IPhysicsActor
    {
        private Body jigLibXBody;
        private CollisionSkin jigLibXSkin;
        private List<ShapeDesc> shapes;

        private Int64 ownerEntityID;
        private FrameworkCore game;

        private Dictionary<Int64, EntityCollisionInfo> currentCollisions;
        private Dictionary<Int64, EntityCollisionInfo> lastFramesCollisions;
        private CollisionCallbackFn listener;
        
        /// <summary>
        /// Constructs a new physics actor.
        /// </summary>
        /// <param name="desc">Descriptor for the actor.</param>
        internal JigLibXActor(ActorDesc desc)
        {
            this.shapes = desc.Shapes;           
            this.ownerEntityID = desc.EntityID;
            this.game = desc.Game;

            bool applyCOM = true;

            // Construct the JigLibX body/skin for this actor.
            if (desc.Character)
            {
                jigLibXBody = new CharacterBody();
                jigLibXBody.AllowFreezing = false;
            }
            else
            {
                jigLibXBody = new Body();
            }

            if (desc.Phantom)
            {
                jigLibXSkin = new PhantomCollisionSkin(this.ownerEntityID, jigLibXBody);
                desc.Dynamic = false; // Phantoms cannot move
                desc.Orientation = Matrix.CreateRotationX(MathHelper.PiOver2);
            }
            else
            {
                jigLibXSkin = new QSCollisionSkin(this.ownerEntityID, jigLibXBody);
            }

            // Build all shapes that make up the actor.
            for(int i = 0; i < desc.Shapes.Count; ++i)
            {
                ShapeDesc shapeDesc = desc.Shapes[i];

                // The basic concept is the same for all shapes: create JigLibX shape of proper size and bind it to the skin.
                // TODO: Implement materials.                

                if(shapeDesc is BoxShapeDesc)
                {
                    BoxShapeDesc boxDesc = shapeDesc as BoxShapeDesc;
                    Box box = new Box(Vector3.Zero, Matrix.Identity, boxDesc.Extents);
                    jigLibXSkin.AddPrimitive(box, (int)MaterialTable.MaterialID.NotBouncyRough);
                }
                else if(shapeDesc is SphereShapeDesc)
                {
                    SphereShapeDesc sphereDesc = shapeDesc as SphereShapeDesc;
                    Sphere sphere = new Sphere(Vector3.Zero, sphereDesc.Radius);
                    jigLibXSkin.AddPrimitive(sphere, (int)MaterialTable.MaterialID.NormalSmooth);
                }
                else if (shapeDesc is CapsuleShapeDesc)
                {
                    CapsuleShapeDesc capDesc = shapeDesc as CapsuleShapeDesc;
                    Capsule cyl = new Capsule(Vector3.Zero, desc.Orientation, capDesc.Radius, capDesc.Length);
                    jigLibXSkin.AddPrimitive(cyl, (int)MaterialTable.MaterialID.NotBouncyNormal);
                }
                else if (shapeDesc is TriangleMeshShapeDesc)
                {
                    TriangleMeshShapeDesc triDesc = shapeDesc as TriangleMeshShapeDesc;

                    TriangleMesh triMesh = new TriangleMesh();

                    List<TriangleVertexIndices> triInds = new List<TriangleVertexIndices>();

                    int numPrimitives = triDesc.Indices.Count;

                    for (int j = 0; j < (numPrimitives - 2); ++j)
                    {
                        TriangleVertexIndices triIndex = new TriangleVertexIndices(triDesc.Indices[j + 2], triDesc.Indices[j + 1], triDesc.Indices[j]);

                        triInds.Add(triIndex);
                    }

                    triMesh.CreateMesh(triDesc.Vertices, triInds, 1024, 64);

                    jigLibXSkin.AddPrimitive(triMesh, (int)MaterialTable.MaterialID.NotBouncyRough);
                }
                else if (shapeDesc is HeightFieldShapeDesc)
                {
                    // For height fields, we need to copy the data into an Array2D.
                    HeightFieldShapeDesc heightFieldDesc = shapeDesc as HeightFieldShapeDesc;
                    Array2D field = new Array2D(heightFieldDesc.HeightField.GetUpperBound(0), heightFieldDesc.HeightField.GetUpperBound(1));

                    for (int x = 0; x < heightFieldDesc.HeightField.GetUpperBound(0); ++x)
                    {
                        for (int z = 0; z < heightFieldDesc.HeightField.GetUpperBound(1); ++z)
                        {
                            field.SetAt(x, z, heightFieldDesc.HeightField[x, z]);
                        }
                    }

                    Heightmap heightmap = new Heightmap(field, heightFieldDesc.SizeX / 2.0f, heightFieldDesc.SizeZ / 2.0f, heightFieldDesc.SizeX / heightFieldDesc.HeightField.GetLength(0), heightFieldDesc.SizeZ / heightFieldDesc.HeightField.GetLength(1));
                    jigLibXSkin.AddPrimitive(heightmap, (int)MaterialTable.MaterialID.NotBouncyRough);

                    applyCOM = false;
                }                
                else
                {
                    throw new Exception("Bad shape.");
                }
            }

            // Finalize the body/skin and set center-of-mass.
            PrimitiveProperties primitiveProperties = new PrimitiveProperties(PrimitiveProperties.MassDistributionEnum.Solid, PrimitiveProperties.MassTypeEnum.Density, desc.Density);

            float junk;
            Vector3 com;
            Matrix it, itCoM;

            jigLibXBody.CollisionSkin = jigLibXSkin;
            jigLibXSkin.GetMassProperties(primitiveProperties, out junk, out com, out it, out itCoM);
            jigLibXBody.BodyInertia = itCoM;
            jigLibXBody.Mass = junk;

            jigLibXBody.MoveTo(desc.Position + com, desc.Orientation);

            if (applyCOM)
            {
                jigLibXSkin.ApplyLocalTransform(new Transform(-com, Matrix.Identity));
            }

            if (desc.Character)
            {
                jigLibXBody.SetBodyInvInertia(0.0f, 0.0f, 0.0f);
            }

            // Let JigLibX know this body should be a part of the simulation.
            jigLibXBody.EnableBody();
            
            jigLibXBody.Immovable = !desc.Dynamic;
        }

        /// <summary>
        /// Releases all unmanaged resources for the actor.
        /// </summary>
        public void Dispose()
        {
        }

        /// <summary>
        /// Gets/sets the actor's density.
        /// </summary>
        public float Density
        {
            get
            {
                // Doesn't seem to be supported by JigLibX.
                // @todo: Write GetDensity methods for every shape type.
                throw new Exception("Not implemented");
            }
            set
            {
                // Reset the body's inertia and mass with the new density value.
                PrimitiveProperties primitiveProperties = new PrimitiveProperties(PrimitiveProperties.MassDistributionEnum.Solid, PrimitiveProperties.MassTypeEnum.Density, value);

                float junk; 
                Vector3 com; 
                Matrix it, itCoM;

                jigLibXSkin.GetMassProperties(primitiveProperties, out junk, out com, out it, out itCoM);
                jigLibXBody.BodyInertia = itCoM;
                jigLibXBody.Mass = junk;
            }
        }

        /// <summary>
        /// Gets/sets the actor's mass.
        /// </summary>
        public float Mass
        {
            get
            {
                return jigLibXBody.Mass;
            }
            set
            {
                // Reset the body's inertia and mass with the new mass value.
                PrimitiveProperties primitiveProperties = new PrimitiveProperties(PrimitiveProperties.MassDistributionEnum.Solid, PrimitiveProperties.MassTypeEnum.Mass, value);

                float junk;
                Vector3 com;
                Matrix it, itCoM;

                jigLibXSkin.GetMassProperties(primitiveProperties, out junk, out com, out it, out itCoM);
                jigLibXBody.BodyInertia = itCoM;
                jigLibXBody.Mass = junk;
            }
        }

        /// <summary>
        /// Gets/sets the actor's position.
        /// </summary>
        public Vector3 Position
        {
            get
            {
                return jigLibXBody.Position;
            }
            set
            {
                jigLibXBody.EnableBody();
                jigLibXBody.Position = value;
            }
        }

        /// <summary>
        /// Gets/sets the actor's orientation.
        /// </summary>
        public Matrix Orientation
        {
            get
            {
                return jigLibXBody.Orientation;
            }
            set
            {
                jigLibXBody.EnableBody();
                jigLibXBody.Orientation = value;
            }
        }

        public Vector3 LinearVelocity
        {
            get
            {
                if (jigLibXBody is CharacterBody)
                {
                    return (jigLibXBody as CharacterBody).DesiredVelocity;
                }
                else
                {
                    return jigLibXBody.Velocity;
                }
            }
            set
            {
                if (jigLibXBody is CharacterBody)
                {
                    (jigLibXBody as CharacterBody).DesiredVelocity = value;
                }
                else
                {
                    jigLibXBody.Velocity = value;
                    jigLibXBody.VelocityAux = value;
                }
            }
        }

        public void AddForceFromOutsideSimulation(Vector3 force)
        {
            jigLibXBody.ApplyWorldImpulse(force); 
        }

        public Vector3 AngularVelocity
        {
            get
            {
                return jigLibXBody.AngularVelocity;
            }
            set
            {
                jigLibXBody.AngularVelocity = value;
            }
        }

        /// <summary>
        /// Gets the list of shapes that make up the actor.
        /// </summary>
        public List<ShapeDesc> Shapes
        {
            get 
            {
                return shapes;
            }
        }

#if !XBOX360
        public VertexPositionColor[] BodyVerts
        {
            get
            {
                VertexPositionColor[] wf = jigLibXSkin.GetLocalSkinWireframe();

                // if the collision skin was also added to the body
                // we have to transform the skin wireframe to the body space
                if (jigLibXBody.CollisionSkin != null)
                {
                    jigLibXBody.TransformWireframe(wf);
                }

                return wf;
            }
        }
#endif //!XBOX360

        /// <summary>
        /// Unique ID of the <see cref="BaseEntity"/> this physics resides on
        /// </summary>
        private Int64 OwnerEntityID
        {
            get { return this.ownerEntityID; }
        }

        /// <summary>
        /// Reference to the game.
        /// </summary>
        public FrameworkCore Game
        {
            get { return this.game; }
        }

        public void EnableCollisionListening()
        {
            // If listener isn't already enabled
            if (listener == null)
            {
                currentCollisions = new Dictionary<Int64, EntityCollisionInfo>();
                lastFramesCollisions = new Dictionary<Int64, EntityCollisionInfo>();

                listener = new CollisionCallbackFn(CollisionHandler);
                jigLibXSkin.callbackFn += new CollisionCallbackFn(CollisionHandler);
            }
        }

        public void DisableCollisionListening()
        {
            // If listener is enabled
            if (listener != null)
            {
                jigLibXSkin.callbackFn -= listener;
                listener = null;

                CompareCollisions();

                currentCollisions.Clear();
                currentCollisions = null;

                lastFramesCollisions.Clear();
                lastFramesCollisions = null;
            }
        }

        public bool CollisionHandler(CollisionSkin self, CollisionSkin other)
        {
            CollisionData data;

            bool SelfIsPhantom = (self is PhantomCollisionSkin);
            bool OtherIsPhantom = (other is PhantomCollisionSkin);

            // Fill in data for the two colliding entities
            if (SelfIsPhantom)
            {
                PhantomCollisionSkin phantom = self as PhantomCollisionSkin;
                data.ListeningEntity.IsPhantom = true;
                data.ListeningEntity.EntityID = phantom.EntityID;
            }
            else // If we ever have more than two skin types, this will need to be an 'else if', but for now we can assume it's a QSCollisionSkin
            {
                QSCollisionSkin qsSkin = self as QSCollisionSkin;
                data.ListeningEntity.IsPhantom = false;
                data.ListeningEntity.EntityID = qsSkin.EntityID;
            }

            if (OtherIsPhantom)
            {
                PhantomCollisionSkin phantom = other as PhantomCollisionSkin;
                data.OtherEntity.IsPhantom = true;
                data.OtherEntity.EntityID = phantom.EntityID;
            }
            else // If we ever have more than two skin types, this will need to be an 'else if', but for now we can assume it's a QSCollisionSkin
            {
                QSCollisionSkin qsSkin = other as QSCollisionSkin;
                data.OtherEntity.IsPhantom = false;
                data.OtherEntity.EntityID = qsSkin.EntityID;
            }

            // A new collision has occured
            EntityCollisionInfo info;
            if ( !currentCollisions.TryGetValue(data.OtherEntity.EntityID, out info) )
            {
                currentCollisions.Add(data.OtherEntity.EntityID, data.OtherEntity);
            }

            // If either skin is a phantom, then no collision happens
            if (SelfIsPhantom || OtherIsPhantom)
            {
                return false;
            }

            // Let collision be handled by physics engine
            return true;
        }

        public void UpdateCollisions()
        {
            if (listener == null)
                return;

            if (currentCollisions.Count == 0 && lastFramesCollisions.Count == 0)
                return;

            CompareCollisions();

            // Clear out last frames collision info, and copy current frames info
            lastFramesCollisions.Clear();
            foreach (EntityCollisionInfo collInfo in currentCollisions.Values)
            {
                lastFramesCollisions.Add(collInfo.EntityID, collInfo);
            }

            currentCollisions.Clear();
        }

        public void RemoveFromSimulation()
        {
            jigLibXBody.DisableBody();
        }

        public void SetMovable(bool movable)
        {
            jigLibXBody.Immovable = !movable;
        }

        public BoundingBox GetBoundingBox()
        {
            return jigLibXSkin.WorldBoundingBox;
        }

        /// <summary>
        /// If the actor is a character, make it jump
        /// </summary>
        public void CharacterDoJump()
        {
            if (jigLibXBody is CharacterBody)
            {
                (jigLibXBody as CharacterBody).DoJump();
            }
        }

        private void CompareCollisions()
        {
            // Compare current collisions with last frames collisions
            Dictionary<Int64, EntityCollisionInfo>.ValueCollection currentValues = currentCollisions.Values;

            EntityCollisionInfo info;
            foreach (EntityCollisionInfo entityInfo in currentValues)
            {
                // If there is a collision that wasn't there last frame, send out a message
                if (!lastFramesCollisions.TryGetValue(entityInfo.EntityID, out info))
                {
                    MsgOnCollision msgCollData = ObjectPool.Aquire<MsgOnCollision>();                    
                    msgCollData.EntityID = entityInfo.EntityID;
                    msgCollData.IsPhantom = entityInfo.IsPhantom;                    
                    msgCollData.UniqueTarget = this.OwnerEntityID;
                    this.game.SendMessage(msgCollData);
                }
            }

            Dictionary<Int64, EntityCollisionInfo>.ValueCollection lastFrameValues = lastFramesCollisions.Values;
            foreach (EntityCollisionInfo entityInfo in lastFrameValues)
            {
                // If there is no longer a collision that was there last frame, send out a message.
                if (!currentCollisions.TryGetValue(entityInfo.EntityID, out info))
                {
                    MsgOffCollision msgCollData = ObjectPool.Aquire<MsgOffCollision>();                    
                    msgCollData.EntityID = entityInfo.EntityID;
                    msgCollData.IsPhantom = entityInfo.IsPhantom;
                    msgCollData.UniqueTarget = this.OwnerEntityID;
                    this.game.SendMessage(msgCollData);
                }
            }
        }
    }
}
